﻿// 2022/4/1: 首个版本
// 2022/10/12: SetConfig过滤相同ID设备
// 2022/11/3: 新增VideoDeviceMode，VideoDeviceConfig.OutputMode
// 2022/11/21: GetConfig增加enabled字段兼容旧版本
// 2022/12/30: SetConfig兼容enabled字段
// 2023/2/20: VideoChannelConfig.RemoteSize/Codec/RecordFPS/AlignedFPS
// 2023/4/10: 增加Load/Save方法
// 2023/7/6: 处理线程数不能超过本机CPU核数的一半，且默认值改为4
// 2023/10/9: 添加英文注释
// 2023/10/20: 修正VFlip方向
// 2024/1/29: GetConfigStatus输出errorHint
// 2024/3/26: Remote相关字段标注为deprecated
// 2024/5/28: 新增CustomizedParameters
// 2024/6/19: 继承ModuleConfig，支持异步
// 2024/7/17: 简化数值配置
// 2024/7/30: 多线程安全
// 2025/2/10: 新增SetChannelDevices
// 2025/5/15: 使用ChannelNumber确定视频通道数

using System;
using System.Collections.Generic;
using System.Xml;
using ASEva;
using ASEva.Utility;
using ASEva.Samples;
using SO = System.Reflection.ObfuscationAttribute;
using System.Threading.Tasks;

namespace CommonSource
{
    [SO]
    enum VideoDeviceMode
    {
        Disabled = 0,
        Input = 1,
        Output = 2,
    }

    class VideoDeviceConfig
    {
        public VideoDeviceMode Mode { get; set; }
        public VideoDeviceID ID { get; set; }
        public int ChannelIndex { get; set; }
        public VideoInputMode InputMode { get; set; }
        public VideoOutputMode OutputMode { get; set; }
        public int FrameRate { get; set; }
        public bool AlignedFPS { get; set; }

        public VideoDeviceConfig()
        {
            Mode = VideoDeviceMode.Disabled;
            InputMode = new VideoInputMode() { InputCodec = VideoDataCodec.H264, Size = new IntSize(640, 480) };
            OutputMode = new VideoOutputMode() { OutputCodec = VideoDataCodec.H264, Size = new IntSize(640, 480) };
            FrameRate = 15;
            AlignedFPS = false;
            ChannelIndex = 0;
        }
    }

    class VideoChannelDevice
    {
        public String Name { get; set; }
        public bool IsOutput { get; set; }
        public VideoDeviceID ID { get; set; }
        public VideoDataCodec Codec { get; set; }
        public IntSize Size { get; set; }

        // The following properties are only available while IsOutput=false
        public int FrameRate { get; set; }
        public bool Invert { get; set; }
        public bool VFlip { get; set; }
        public double HFOV { get; set; }
    }

    interface VideoParameters
    {
        SpecialCameraType GetSpecialType();
        void ToXml(XmlElement node);
        void FromXml(XmlElement node);
        void Reset();
    }

    static class VideoParameterParserExtension
    {
        public static double ParseVideoParameter(this AttributeParser parser, String key, double defaultVal)
        {
            var val = parser.ParseString(key, null);
            if (val == null) return defaultVal;

            var comps = val.Split(':');
            if (comps.Length > 2) return defaultVal;
            else if (comps.Length == 2) val = comps[1];

            double output;
            if (Double.TryParse(val, out output)) return output;
            else return defaultVal;
        }
    }

    class PinholeIntrinsicsParameters
    {
        public double Invert { get; set; }
        public double VFlip { get; set; }
        public double Undistort { get; set; }
        public double HFOV { get; set; }
        public double AspectRatio { get; set; }
        public double CU { get; set; }
        public double CV { get; set; }
        public double K1 { get; set; }
        public double K2 { get; set; }
        public double K3 { get; set; }
        public double K4 { get; set; }
        public double K5 { get; set; }
        public double K6 { get; set; }
        public double P1 { get; set; }
        public double P2 { get; set; }

        public PinholeIntrinsicsParameters()
        {
            ResetAllIntrinsics();
        }

        public void PinholeIntrinsicsToXml(XmlElement node)
        {
            var cw = new AttributeWriter(node);
            cw.WriteDouble("inversion", Invert);
            cw.WriteDouble("vertical_flip", VFlip);
            var legacyInvert = (int)Invert ^ (int)VFlip;
            cw.WriteDouble("invert", legacyInvert);
            cw.WriteDouble("vflip", VFlip);
            cw.WriteDouble("undistort", Undistort);
            cw.WriteDouble("hfov", HFOV);
            cw.WriteDouble("aspectratio", AspectRatio);
            cw.WriteDouble("cu", CU);
            cw.WriteDouble("cv", CV);
            cw.WriteDouble("k1", K1);
            cw.WriteDouble("k2", K2);
            cw.WriteDouble("k3", K3);
            cw.WriteDouble("k4", K4);
            cw.WriteDouble("k5", K5);
            cw.WriteDouble("k6", K6);
            cw.WriteDouble("p1", P1);
            cw.WriteDouble("p2", P2);
        }

        public void PinholeIntrinsicsFromXml(XmlElement node)
        {
            var cp = new AttributeParser(node);
            if (node.Attributes["inversion"] != null)
            {
                Invert = cp.ParseVideoParameter("inversion", 0);
                VFlip = cp.ParseVideoParameter("vertical_flip", 0);
            }
            else
            {
                var rawInvert = cp.ParseVideoParameter("invert", 0);
                VFlip = cp.ParseVideoParameter("vflip", 0);
                Invert = (int)rawInvert ^ (int)VFlip;
            }
            Undistort = cp.ParseVideoParameter("undistort", 0);
            HFOV = cp.ParseVideoParameter("hfov", 45);
            AspectRatio = cp.ParseVideoParameter("aspectratio", 1);
            CU = cp.ParseVideoParameter("cu", 0);
            CV = cp.ParseVideoParameter("cv", 0);
            K1 = cp.ParseVideoParameter("k1", 0);
            K2 = cp.ParseVideoParameter("k2", 0);
            K3 = cp.ParseVideoParameter("k3", 0);
            K4 = cp.ParseVideoParameter("k4", 0);
            K5 = cp.ParseVideoParameter("k5", 0);
            K6 = cp.ParseVideoParameter("k6", 0);
            P1 = cp.ParseVideoParameter("p1", 0);
            P2 = cp.ParseVideoParameter("p2", 0);
        }

        public void ResetAllIntrinsics()
        {
            Invert = 0;
            VFlip = 0;
            Undistort = 0;
            HFOV = 45;
            AspectRatio = 1;
            CU = 0;
            CV = 0;
            K1 = 0;
            K2 = 0;
            K3 = 0;
            K4 = 0;
            K5 = 0;
            K6 = 0;
            P1 = 0;
            P2 = 0;
        }

        public bool IsUndistortNecessary()
        {
            return AspectRatio != 1 || K1 != 0 || K2 != 0 || K3 != 0 || K4 != 0 || K5 != 0 || K6 != 0 || P1 != 0 || P2 != 0;
        }
    }

    class NormalVideoParameters : PinholeIntrinsicsParameters, VideoParameters
    {
        public double RectifyRoll { get; set; }
        public double RectifyPitch { get; set; }
        public double RectifyYaw { get; set; }
        public double OX { get; set; }
        public double OY { get; set; }
        public double OZ { get; set; }
        public double Yaw { get; set; }
        public double Pitch { get; set; }
        public double Roll { get; set; }
        public double SpecialHint { get; set; } // 0: normal, 1: any, 2: front

        public double RectifiedYaw
        {
            get
            {
                if (Yaw >= -90 && Yaw <= 90) return 0;
                else if (Yaw > 90 || Yaw < -90) return 180;
                else return 0;
            }
        }

        public NormalVideoParameters()
        {
            Reset();
        }

        public SpecialCameraType GetSpecialType()
        {
            switch ((int)SpecialHint)
            {
                case 0:
                    return SpecialCameraType.Normal;
                case 1:
                    return SpecialCameraType.AnyCamera;
                case 2:
                    return SpecialCameraType.FrontCamera;
                default:
                    return SpecialCameraType.Normal;
            }
        }

        public void ToXml(XmlElement node)
        {
            PinholeIntrinsicsToXml(node);

            var cw = new AttributeWriter(node);
            cw.WriteDouble("rectify_roll", RectifyRoll);
            cw.WriteDouble("rectify_pitch", RectifyPitch);
            cw.WriteDouble("rectify_yaw", RectifyYaw);
            cw.WriteDouble("rectified_yaw", RectifiedYaw);
            cw.WriteDouble("ox", OX);
            cw.WriteDouble("oy", OY);
            cw.WriteDouble("oz", OZ);
            cw.WriteDouble("yaw", Yaw);
            cw.WriteDouble("pitch", Pitch);
            cw.WriteDouble("roll", Roll);
            cw.WriteDouble("special_hint", SpecialHint);
        }

        public void FromXml(XmlElement node)
        {
            PinholeIntrinsicsFromXml(node);

            var cp = new AttributeParser(node);
            RectifyRoll = cp.ParseVideoParameter("rectify_roll", 0);
            RectifyPitch = cp.ParseVideoParameter("rectify_pitch", 0);
            RectifyYaw = cp.ParseVideoParameter("rectify_yaw", 0);
            OX = cp.ParseVideoParameter("ox", 0);
            OY = cp.ParseVideoParameter("oy", 0);
            OZ = cp.ParseVideoParameter("oz", 1);
            Yaw = cp.ParseVideoParameter("yaw", 0);
            Pitch = cp.ParseVideoParameter("pitch", 0);
            Roll = cp.ParseVideoParameter("roll", 0);
            SpecialHint = cp.ParseVideoParameter("special_hint", 0);
        }


        public void Reset()
        {
            ResetAllIntrinsics();

            RectifyRoll = 0;
            RectifyPitch = 0;
            RectifyYaw = 0;
            OX = 0;
            OY = 0;
            OZ = 1;
            Yaw = 0;
            Pitch = 0;
            Roll = 0;
            SpecialHint = 0;
        }
    }

    class LaneLineVideoParameters : PinholeIntrinsicsParameters, VideoParameters
    {
        public double ZeroPointU { get; set; }
        public double ZeroPointV { get; set; }
        public double MeterPointU { get; set; }
        public double MeterPointV { get; set; }
        public double FiftyRatio { get; set; }
        public double MeterScale { get; set; }
        public double SpecialHint { get; set; } // 0: left, 1: right

        public LaneLineVideoParameters()
        {
            Reset();
        }

        public SpecialCameraType GetSpecialType()
        {
            return SpecialHint == 1 ? SpecialCameraType.RightLaneCamera : SpecialCameraType.LeftLaneCamera;
        }

        public void ToXml(XmlElement node)
        {
            PinholeIntrinsicsToXml(node);

            var cw = new AttributeWriter(node);
            cw.WriteDouble("zero_u", ZeroPointU);
            cw.WriteDouble("zero_v", ZeroPointV);
            cw.WriteDouble("meter_u", MeterPointU);
            cw.WriteDouble("meter_v", MeterPointV);
            cw.WriteDouble("fifty", FiftyRatio);
            cw.WriteDouble("meter_scale", MeterScale);
            cw.WriteDouble("special_hint", SpecialHint);
        }

        public void FromXml(XmlElement node)
        {
            PinholeIntrinsicsFromXml(node);

            var cp = new AttributeParser(node);
            ZeroPointU = cp.ParseVideoParameter("zero_u", 0.25);
            ZeroPointV = cp.ParseVideoParameter("zero_v", 0);
            MeterPointU = cp.ParseVideoParameter("meter_u", -0.25);
            MeterPointV = cp.ParseVideoParameter("meter_v", 0);
            FiftyRatio = cp.ParseVideoParameter("fifty", 0.5);
            MeterScale = cp.ParseVideoParameter("meter_scale", 1);
            SpecialHint = cp.ParseVideoParameter("special_hint", 0);
        }

        public void Reset()
        {
            ResetAllIntrinsics();

            ZeroPointU = 0.25;
            ZeroPointV = 0;
            MeterPointU = -0.25;
            MeterPointV = 0;
            FiftyRatio = 0.5;
            MeterScale = 1;
            SpecialHint = 0;
        }
    }

    class BlindSpotParameters : PinholeIntrinsicsParameters, VideoParameters
    {
        public double P00U { get; set; }
        public double P00V { get; set; }
        public double P04U { get; set; }
        public double P04V { get; set; }
        public double P30U { get; set; }
        public double P30V { get; set; }
        public double P34U { get; set; }
        public double P34V { get; set; }
        public double P0hRatio { get; set; }
        public double P3hRatio { get; set; }
        public double Ph0Ratio { get; set; }
        public double Ph4Ratio { get; set; }
        public double SpecialHint { get; set; } // 0: left, 1: right

        public BlindSpotParameters()
        {
            Reset();
        }

        public SpecialCameraType GetSpecialType()
        {
            return SpecialHint == 1 ? SpecialCameraType.RightBlindSpotCamera : SpecialCameraType.LeftBlindSpotCamera;
        }

        public void ToXml(XmlElement node)
        {
            PinholeIntrinsicsToXml(node);

            var cw = new AttributeWriter(node);
            cw.WriteDouble("p00_u", P00U);
            cw.WriteDouble("p00_v", P00V);
            cw.WriteDouble("p04_u", P04U);
            cw.WriteDouble("p04_v", P04V);
            cw.WriteDouble("p30_u", P30U);
            cw.WriteDouble("p30_v", P30V);
            cw.WriteDouble("p34_u", P34U);
            cw.WriteDouble("p34_v", P34V);
            cw.WriteDouble("p0h", P0hRatio);
            cw.WriteDouble("p3h", P3hRatio);
            cw.WriteDouble("ph0", Ph0Ratio);
            cw.WriteDouble("ph4", Ph4Ratio);
            cw.WriteDouble("special_hint", SpecialHint);
        }

        public void FromXml(XmlElement node)
        {
            PinholeIntrinsicsFromXml(node);

            var cp = new AttributeParser(node);
            P00U = cp.ParseVideoParameter("p00_u", -0.25);
            P00V = cp.ParseVideoParameter("p00_v", 0.25);
            P04U = cp.ParseVideoParameter("p04_u", -0.25);
            P04V = cp.ParseVideoParameter("p04_v", -0.25);
            P30U = cp.ParseVideoParameter("p30_u", 0.25);
            P30V = cp.ParseVideoParameter("p30_v", 0.25);
            P34U = cp.ParseVideoParameter("p34_u", 0.25);
            P34V = cp.ParseVideoParameter("p34_v", -0.25);
            P0hRatio = cp.ParseVideoParameter("p0h", 0.5);
            P3hRatio = cp.ParseVideoParameter("p3h", 0.5);
            Ph0Ratio = cp.ParseVideoParameter("ph0", 0.5);
            Ph4Ratio = cp.ParseVideoParameter("ph4", 0.5);
            SpecialHint = cp.ParseVideoParameter("special_hint", 0);
        }

        public void Reset()
        {
            ResetAllIntrinsics();

            P00U = -0.25;
            P00V = 0.25;
            P04U = -0.25;
            P04V = -0.25;
            P30U = 0.25;
            P30V = 0.25;
            P34U = 0.25;
            P34V = -0.25;
            P0hRatio = 0.5;
            P3hRatio = 0.5;
            Ph0Ratio = 0.5;
            Ph4Ratio = 0.5;
            SpecialHint = 0;
        }
    }

    class FisheyeVideoParameters : VideoParameters
    {
        public double Invert { get; set; }
        public double VFlip { get; set; }
        public double Undistort { get; set; }
        public double HFOV { get; set; }
        public double HFOVFisheye { get; set; }
        public double AspectRatio { get; set; }
        public double CU { get; set; }
        public double CV { get; set; }
        public double K1 { get; set; }
        public double K2 { get; set; }
        public double K3 { get; set; }
        public double K4 { get; set; }

        public double RectifyRoll { get; set; }
        public double RectifyPitch { get; set; }
        public double RectifyYaw { get; set; }
        public double OX { get; set; }
        public double OY { get; set; }
        public double OZ { get; set; }
        public double Yaw { get; set; }
        public double Pitch { get; set; }
        public double Roll { get; set; }
        public double SpecialHint { get; set; } // 0: normal, 1: any, 2: front

        public FisheyeVideoParameters()
        {
            Reset();
        }

        public void ResetAllIntrinsics()
        {
            Invert = 0;
            VFlip = 0;
            Undistort = 0;
            HFOV = 120;
            HFOVFisheye = 150;
            AspectRatio = 1;
            CU = 0;
            CV = 0;
            K1 = 0;
            K2 = 0;
            K3 = 0;
            K4 = 0;
        }

        public bool IsUndistortNecessary()
        {
            return AspectRatio != 1 || K1 != 0 || K2 != 0 || K3 != 0 || K4 != 0 || HFOV != HFOVFisheye;
        }

        public double RectifiedYaw
        {
            get
            {
                if (Yaw >= -90 && Yaw <= 90) return 0;
                else if (Yaw > 90 || Yaw < -90) return 180;
                else return 0;
            }
        }

        public SpecialCameraType GetSpecialType()
        {
            switch ((int)SpecialHint)
            {
                case 0:
                    return SpecialCameraType.NormalFisheyeCamera;
                case 1:
                    return SpecialCameraType.AnyFisheyeCamera;
                case 2:
                    return SpecialCameraType.FrontFisheyeCamera;
                default:
                    return SpecialCameraType.NormalFisheyeCamera;
            }
        }

        public void ToXml(XmlElement node)
        {
            var cw = new AttributeWriter(node);

            cw.WriteDouble("inversion", Invert);
            cw.WriteDouble("vertical_flip", VFlip);
            var legacyInvert = (int)Invert ^ (int)VFlip;
            cw.WriteDouble("invert", legacyInvert);
            cw.WriteDouble("vflip", VFlip);
            cw.WriteDouble("undistort", Undistort);
            cw.WriteDouble("hfov", HFOV);
            cw.WriteDouble("hfov_fisheye", HFOVFisheye);
            cw.WriteDouble("aspectratio", AspectRatio);
            cw.WriteDouble("cu", CU);
            cw.WriteDouble("cv", CV);
            cw.WriteDouble("k1", K1);
            cw.WriteDouble("k2", K2);
            cw.WriteDouble("k3", K3);
            cw.WriteDouble("k4", K4);

            cw.WriteDouble("rectify_roll", RectifyRoll);
            cw.WriteDouble("rectify_pitch", RectifyPitch);
            cw.WriteDouble("rectify_yaw", RectifyYaw);
            cw.WriteDouble("rectified_yaw", RectifiedYaw);
            cw.WriteDouble("ox", OX);
            cw.WriteDouble("oy", OY);
            cw.WriteDouble("oz", OZ);
            cw.WriteDouble("yaw", Yaw);
            cw.WriteDouble("pitch", Pitch);
            cw.WriteDouble("roll", Roll);
            cw.WriteDouble("special_hint", SpecialHint);
        }

        public void FromXml(XmlElement node)
        {
            var cp = new AttributeParser(node);

            if (node.Attributes["inversion"] != null)
            {
                Invert = cp.ParseVideoParameter("inversion", 0);
                VFlip = cp.ParseVideoParameter("vertical_flip", 0);
            }
            else
            {
                var rawInvert = cp.ParseVideoParameter("invert", 0);
                VFlip = cp.ParseVideoParameter("vflip", 0);
                Invert = (int)rawInvert ^ (int)VFlip;
            }
            Undistort = cp.ParseVideoParameter("undistort", 0);
            HFOV = cp.ParseVideoParameter("hfov", 120);
            HFOVFisheye = cp.ParseVideoParameter("hfov_fisheye", 150);
            AspectRatio = cp.ParseVideoParameter("aspectratio", 1);
            CU = cp.ParseVideoParameter("cu", 0);
            CV = cp.ParseVideoParameter("cv", 0);
            K1 = cp.ParseVideoParameter("k1", 0);
            K2 = cp.ParseVideoParameter("k2", 0);
            K3 = cp.ParseVideoParameter("k3", 0);
            K4 = cp.ParseVideoParameter("k4", 0);

            RectifyRoll = cp.ParseVideoParameter("rectify_roll", 0);
            RectifyPitch = cp.ParseVideoParameter("rectify_pitch", 0);
            RectifyYaw = cp.ParseVideoParameter("rectify_yaw", 0);
            OX = cp.ParseVideoParameter("ox", 0);
            OY = cp.ParseVideoParameter("oy", 0);
            OZ = cp.ParseVideoParameter("oz", 1);
            Yaw = cp.ParseVideoParameter("yaw", 0);
            Pitch = cp.ParseVideoParameter("pitch", 0);
            Roll = cp.ParseVideoParameter("roll", 0);
            SpecialHint = cp.ParseVideoParameter("special_hint", 0);
        }

        public void Reset()
        {
            ResetAllIntrinsics();

            RectifyRoll = 0;
            RectifyPitch = 0;
            RectifyYaw = 0;
            OX = 0;
            OY = 0;
            OZ = 1;
            Yaw = 0;
            Pitch = 0;
            Roll = 0;
            SpecialHint = 0;
        }
    }

    class CustomizedMap
    {
        public float[] USampleCoords { get; set; } // -0.5~+0.5
        public float[] VSampleCoords { get; set; }
        public float[,] UMap { get; set; } // [v, u]
        public float[,] VMap { get; set; } // [v, u]

        public CustomizedMap(int uSampleCount, int vSampleCount)
        {
            USampleCoords = new float[uSampleCount];
            VSampleCoords = new float[vSampleCount];
            UMap = new float[vSampleCount, uSampleCount];
            VMap = new float[vSampleCount, uSampleCount];
        }
    }

    class CustomizedParameters : VideoParameters
    {
        public double Invert { get; set; }
        public double VFlip { get; set; }
        public double Undistort { get; set; }
        public double HFOV { get; set; }
        public CustomizedMap Map { get; set; }

        public double RectifyRoll { get; set; }
        public double RectifyPitch { get; set; }
        public double RectifyYaw { get; set; }
        public double OX { get; set; }
        public double OY { get; set; }
        public double OZ { get; set; }
        public double Yaw { get; set; }
        public double Pitch { get; set; }
        public double Roll { get; set; }

        public CustomizedParameters()
        {
            Reset();
        }

        public void ResetAllIntrinsics()
        {
            Invert = 0;
            VFlip = 0;
            Undistort = 0;
            HFOV = 120;
            Map = null;
        }

        public bool IsUndistortNecessary()
        {
            return Map != null;
        }

        public double RectifiedYaw
        {
            get
            {
                if (Yaw >= -90 && Yaw <= 90) return 0;
                else if (Yaw > 90 || Yaw < -90) return 180;
                else return 0;
            }
        }

        public SpecialCameraType GetSpecialType()
        {
            return SpecialCameraType.CustomizedCamera;
        }

        public void ToXml(XmlElement node)
        {
            var cw = new AttributeWriter(node);

            cw.WriteDouble("inversion", Invert);
            cw.WriteDouble("vertical_flip", VFlip);
            cw.WriteDouble("undistort", Undistort);
            cw.WriteDouble("hfov", HFOV);

            cw.WriteDouble("rectify_roll", RectifyRoll);
            cw.WriteDouble("rectify_pitch", RectifyPitch);
            cw.WriteDouble("rectify_yaw", RectifyYaw);
            cw.WriteDouble("rectified_yaw", RectifiedYaw);
            cw.WriteDouble("ox", OX);
            cw.WriteDouble("oy", OY);
            cw.WriteDouble("oz", OZ);
            cw.WriteDouble("yaw", Yaw);
            cw.WriteDouble("pitch", Pitch);
            cw.WriteDouble("roll", Roll);

            if (Map != null)
            {
                var mapNode = node.AppendChild(node.OwnerDocument.CreateElement("map")) as XmlElement;
                mapNode.Attributes.Append(node.OwnerDocument.CreateAttribute("u_coords")).Value = String.Join(',', Map.USampleCoords);
                mapNode.Attributes.Append(node.OwnerDocument.CreateAttribute("v_coords")).Value = String.Join(',', Map.VSampleCoords);
                float[] uMapArray = new float[Map.UMap.GetLength(0) * Map.UMap.GetLength(1)], vMapArray = new float[Map.VMap.GetLength(0) * Map.VMap.GetLength(1)];
                Buffer.BlockCopy(Map.UMap, 0, uMapArray, 0, uMapArray.Length * sizeof(float));
                Buffer.BlockCopy(Map.VMap, 0, vMapArray, 0, vMapArray.Length * sizeof(float));
                mapNode.InnerText = String.Join(',', uMapArray) + ";" + String.Join(',', vMapArray);
            }
        }

        public void FromXml(XmlElement node)
        {
            var cp = new AttributeParser(node);

            Invert = cp.ParseVideoParameter("inversion", 0);
            VFlip = cp.ParseVideoParameter("vertical_flip", 0);
            Undistort = cp.ParseVideoParameter("undistort", 0);
            HFOV = cp.ParseVideoParameter("hfov", 120);

            RectifyRoll = cp.ParseVideoParameter("rectify_roll", 0);
            RectifyPitch = cp.ParseVideoParameter("rectify_pitch", 0);
            RectifyYaw = cp.ParseVideoParameter("rectify_yaw", 0);
            OX = cp.ParseVideoParameter("ox", 0);
            OY = cp.ParseVideoParameter("oy", 0);
            OZ = cp.ParseVideoParameter("oz", 1);
            Yaw = cp.ParseVideoParameter("yaw", 0);
            Pitch = cp.ParseVideoParameter("pitch", 0);
            Roll = cp.ParseVideoParameter("roll", 0);

            Map = null;
            var mapNodes = node.GetElementsByTagName("map");
            if (mapNodes.Count > 0)
            {
                var mapNode = mapNodes[0];
                var uCoordsElems = mapNode.Attributes["u_coords"].Value.Split(',');
                var vCoordsElems = mapNode.Attributes["v_coords"].Value.Split(',');
                var mapComps = mapNode.InnerText.Split(';');
                var uMapElems = mapComps[0].Split(',');
                var vMapElems = mapComps[1].Split(',');
                if (uCoordsElems.Length > 0 && vCoordsElems.Length > 0 && 
                    uMapElems.Length == uCoordsElems.Length * vCoordsElems.Length &&
                    vMapElems.Length == uCoordsElems.Length * vCoordsElems.Length)
                {
                    Map = new CustomizedMap(uCoordsElems.Length, vCoordsElems.Length);
                    for (int i = 0; i < Map.USampleCoords.Length; i++) Map.USampleCoords[i] = Convert.ToSingle(uCoordsElems[i]);
                    for (int i = 0; i < Map.VSampleCoords.Length; i++) Map.VSampleCoords[i] = Convert.ToSingle(vCoordsElems[i]);
                    int k = 0;
                    for (int v = 0; v < vCoordsElems.Length; v++)
                    {
                        for (int u = 0; u < uCoordsElems.Length; u++)
                        {
                            Map.UMap[v, u] = Convert.ToSingle(uMapElems[k]);
                            Map.VMap[v, u] = Convert.ToSingle(vMapElems[k]);
                            k++;
                        }
                    }
                }
            }
        }

        public void Reset()
        {
            ResetAllIntrinsics();

            RectifyRoll = 0;
            RectifyPitch = 0;
            RectifyYaw = 0;
            OX = 0;
            OY = 0;
            OZ = 1;
            Yaw = 0;
            Pitch = 0;
            Roll = 0;
        }
    }

    class VideoChannelConfig
    {
        public VideoChannelConfig()
        {
            Name = "Unknown";
            Parameters = new NormalVideoParameters();
        }

        public String Name { get; set; }

        /*
         * special type (C#)    -> parameters (C#)         -> meta protocol (C++) -> meta protocol (C#)
         * ----------------------------------------------------------------------------------------------
         * Normal               -> NormalVideoParameters   -> default-v2          -> DefaultCameraMeta
         * AnyCamera            -> NormalVideoParameters   -> generic-v2          -> GenericCameraMeta
         * FrontCamera          -> NormalVideoParameters   -> generic-v2          -> GenericCameraMeta
         * LeftLaneCamera       -> LaneLineVideoParameters -> line-v3             -> LaneLineCameraMeta
         * RightLaneCamera      -> LaneLineVideoParameters -> line-v3             -> LaneLineCameraMeta
         * LeftBlindSpotCamera  -> BlindSpotParameters     -> bs-v3               -> BlindSpotCameraMeta
         * RightBlindSpotCamera -> BlindSpotParameters     -> bs-v3               -> BlindSpotCameraMeta
         * NormalFisheyeCamera  -> FisheyeVideoParameters  -> fisheye-v1          -> FisheyeCameraMeta
         * FrontFisheyeCamera   -> FisheyeVideoParameters  -> fisheye-v1          -> FisheyeCameraMeta
         * AnyFisheyeCamera     -> FisheyeVideoParameters  -> fisheye-v1          -> FisheyeCameraMeta
         * CustomizedCamera     -> CustomizedParameters    -> customized-v1       -> CustomizedCameraMeta
         */
        public VideoParameters Parameters { get; set; }

        public IntSize RemoteSize { get; set; } // deprecated
        public VideoDataCodec RemoteCodec { get; set; } // deprecated
        public int RemoteRecordFPS { get; set; } // deprecated
        public bool RemoteAlignedFPS { get; set; } // deprecated
    }

    class VideoConfig : ModuleConfig
    {
        public VideoConfig()
        {
            SetConfig(Xml.Create("root").InnerXml);
        }

        public static async Task<VideoConfig> Load(object caller)
        {
            var config = new VideoConfig();
            var configString = await AgencyAsync.GetModuleConfig(caller, "video.system");
            config.SetConfig(configString);
            return config;
        }

        public async Task Save(object caller)
        {
            await AgencyAsync.SetModuleConfig(caller, "video.system", GetConfig());
        }

        public override void SetConfig(String config)
        {
            if (String.IsNullOrEmpty(config)) return;
            
            var xml = new XmlDocument();
            xml.LoadXml(config);
            var rootNode = xml.DocumentElement;

            var rootCP = new AttributeParser(rootNode);
            HiresBufferMaxSize = rootCP.ParseLong("hires_buffer_max_size", 500000000);
            MaxProcessThreadCount = Math.Max(1, Math.Min(Environment.ProcessorCount / 2, rootCP.ParseInt("max_process_thread_count", 4)));

            var deviceNodes = rootNode.GetElementsByTagName("device");
            var devices = new List<VideoDeviceConfig>();
            foreach (XmlElement node in deviceNodes)
            {
                var cp = new AttributeParser(node);
                var dev = new VideoDeviceConfig();
                dev.ID = new VideoDeviceID();
                dev.ID.Type = cp.ParseString("cam_type", null);

                int dummy;
                if (dev.ID.Type == null || Int32.TryParse(dev.ID.Type, out dummy)) continue;

                dev.ID.LocalID = cp.ParseString("local_id", null);
                if (dev.ID.LocalID == null || dev.ID.LocalID.Length == 0) continue;

                int mode = cp.ParseInt("mode", 0);
                dev.Mode = (VideoDeviceMode)Math.Max(0, mode);

                var legacyEnabled = cp.ParseBool("enabled", "yes", false);
                if (dev.Mode == VideoDeviceMode.Disabled && legacyEnabled) dev.Mode = VideoDeviceMode.Input;

                dev.ChannelIndex = Math.Max(0, Math.Min(ChannelNumber.Video - 1, cp.ParseInt("channel", 0)));
                var fpsInt = cp.ParseInt("fps", 15);
                if (fpsInt == 0)
                {
                    dev.FrameRate = 15;
                    dev.AlignedFPS = false;
                }
                else
                {
                    dev.FrameRate = Math.Max(10, Math.Min(120, fpsInt));
                    dev.AlignedFPS = cp.ParseBool("aligned_fps", "yes", false);
                }

                var inputCodec = (VideoDataCodec)cp.ParseInt("input_codec", (int)VideoDataCodec.Invalid);
                if (inputCodec != VideoDataCodec.Invalid)
                {
                    dev.InputMode = new VideoInputMode()
                    {
                        InputCodec = inputCodec,
                        Size = new IntSize(cp.ParseInt("input_width", 0), cp.ParseInt("input_height", 0)),
                    };
                    if (dev.InputMode.Size.Width == 0 || dev.InputMode.Size.Height == 0) continue;
                }

                var outputCodec = (VideoDataCodec)cp.ParseInt("output_codec", (int)VideoDataCodec.Invalid);
                if (outputCodec != VideoDataCodec.Invalid)
                {
                    dev.OutputMode = new VideoOutputMode()
                    {
                        OutputCodec = outputCodec,
                        Size = new IntSize(cp.ParseInt("output_width", 0), cp.ParseInt("output_height", 0)),
                    };
                    if (dev.OutputMode.Size.Width == 0 || dev.OutputMode.Size.Height == 0) continue;
                }

                if (devices.Exists(d => d.ID.Equals(dev.ID))) continue;

                devices.Add(dev);
            }

            var channels = new VideoChannelConfig[ChannelNumber.Video];
            var oldStyle = new bool[24];
            for (int i = 0; i < ChannelNumber.Video; i++)
            {
                if (i < 24) oldStyle[i] = true;

                channels[i] = new VideoChannelConfig();
                channels[i].Name = "Channel " + (char)('A' + i);

                var chNodes = rootNode.GetElementsByTagName("ch" + i);
                if (chNodes.Count == 0) continue;

                var chNode = chNodes[0] as XmlElement;
                var cp = new AttributeParser(chNode);

                var fpsInt = cp.ParseInt("remote_rec_fps", 15);
                if (fpsInt == 0)
                {
                    channels[i].RemoteRecordFPS = 15;
                    channels[i].RemoteAlignedFPS = false;
                }
                else
                {
                    channels[i].RemoteRecordFPS = Math.Max(10, Math.Min(120, fpsInt));
                    channels[i].RemoteAlignedFPS = cp.ParseBool("remote_aligned_fps", "yes", false);
                }

                channels[i].RemoteCodec = (VideoDataCodec)cp.ParseInt("remote_codec", (int)VideoDataCodec.Invalid);
                if (channels[i].RemoteCodec != VideoDataCodec.Invalid)
                {
                    var width = cp.ParseInt("remote_width", 0);
                    var height = cp.ParseInt("remote_height", 0);
                    if (width <= 0 || height <= 0) channels[i].RemoteCodec = VideoDataCodec.Invalid;
                    channels[i].RemoteSize = new IntSize(width, height);
                }

                var paramNodes = chNode.GetElementsByTagName("param");
                if (paramNodes.Count == 0) continue;

                var type = cp.ParseString("param_type", null);
                if (type == null) continue;

                channels[i].Name = cp.ParseString("name", channels[i].Name);

                var paramNode = paramNodes[0] as XmlElement;
                switch (type)
                {
                    case "line":
                        channels[i].Parameters = new LaneLineVideoParameters();
                        channels[i].Parameters.FromXml(paramNode);
                        break;
                    case "bs":
                        channels[i].Parameters = new BlindSpotParameters();
                        channels[i].Parameters.FromXml(paramNode);
                        break;
                    case "fisheye":
                        channels[i].Parameters = new FisheyeVideoParameters();
                        channels[i].Parameters.FromXml(paramNode);
                        break;
                    case "custom":
                        channels[i].Parameters = new CustomizedParameters();
                        channels[i].Parameters.FromXml(paramNode);
                        break;
                    default: // "normal"
                        channels[i].Parameters = new NormalVideoParameters();
                        channels[i].Parameters.FromXml(paramNode);
                        break;
                }

                oldStyle[i] = false;
            }

            // The following is for compatibility / 以下为向后兼容部分
            var oldStyleSpecialType = new SpecialCameraType[24];
            var frontCameraNodes = rootNode.GetElementsByTagName("front_camera");
            if (frontCameraNodes.Count > 0)
            {
                var cp = new AttributeParser(frontCameraNodes[0] as XmlElement);
                int frontCameraChannel = cp.ParseInt("channel", -1);
                if (frontCameraChannel >= 0 && frontCameraChannel < oldStyleSpecialType.Length) oldStyleSpecialType[frontCameraChannel] = SpecialCameraType.FrontCamera;

            }
            var leftLaneCameraNodes = rootNode.GetElementsByTagName("left_lane_camera");
            if (leftLaneCameraNodes.Count > 0)
            {
                var cp = new AttributeParser(leftLaneCameraNodes[0] as XmlElement);
                int leftLaneCameraChannel = cp.ParseInt("channel", -1);
                if (leftLaneCameraChannel >= 0 && leftLaneCameraChannel < oldStyleSpecialType.Length) oldStyleSpecialType[leftLaneCameraChannel] = SpecialCameraType.LeftLaneCamera;
            }
            var rightLaneCameraNodes = rootNode.GetElementsByTagName("right_lane_camera");
            if (rightLaneCameraNodes.Count > 0)
            {
                var cp = new AttributeParser(rightLaneCameraNodes[0] as XmlElement);
                int rightLaneCameraChannel = cp.ParseInt("channel", -1);
                if (rightLaneCameraChannel >= 0 && rightLaneCameraChannel < oldStyleSpecialType.Length) oldStyleSpecialType[rightLaneCameraChannel] = SpecialCameraType.RightLaneCamera;
            }
            var leftBSCameraNodes = rootNode.GetElementsByTagName("left_bs_camera");
            if (leftBSCameraNodes.Count > 0)
            {
                var cp = new AttributeParser(leftBSCameraNodes[0] as XmlElement);
                int leftBSCameraChannel = cp.ParseInt("channel", -1);
                if (leftBSCameraChannel >= 0 && leftBSCameraChannel < oldStyleSpecialType.Length) oldStyleSpecialType[leftBSCameraChannel] = SpecialCameraType.LeftBlindSpotCamera;
            }
            var rightBSCameraNodes = rootNode.GetElementsByTagName("right_bs_camera");
            if (rightBSCameraNodes.Count > 0)
            {
                var cp = new AttributeParser(rightBSCameraNodes[0] as XmlElement);
                int rightBSCameraChannel = cp.ParseInt("channel", -1);
                if (rightBSCameraChannel >= 0 && rightBSCameraChannel < oldStyleSpecialType.Length) oldStyleSpecialType[rightBSCameraChannel] = SpecialCameraType.RightBlindSpotCamera;
            }

            for (int i = 0; i < 24; i++)
            {
                if (!oldStyle[i]) continue;

                channels[i] = new VideoChannelConfig();
                channels[i].Name = "Channel " + (char)('A' + i);

                var chNodes = rootNode.GetElementsByTagName("ch" + i);
                if (chNodes.Count == 0) continue;

                var chNode = chNodes[0] as XmlElement;

                var cpMain = new AttributeParser(chNode);
                channels[i].Name = cpMain.ParseString("name", channels[i].Name);

                bool invert = false, vflip = false;
                if (chNode.Attributes["rotate"] == null)
                {
                    invert = cpMain.ParseBool("flip", "yes", false);
                    vflip = false;
                }
                else
                {
                    var rawInvert = cpMain.ParseBool("rotate", "yes", false);
                    vflip = cpMain.ParseBool("vflip", "yes", false);
                    invert = rawInvert ^ vflip;
                }

                bool undistort = cpMain.ParseBool("undistort", "yes", false);

                double hfov = 45, asp = 1, cu = 0, cv = 0, k1 = 0, k2 = 0, k3 = 0, k4 = 0, k5 = 0, k6 = 0, p1 = 0, p2 = 0;
                var cipNodes = chNode.GetElementsByTagName("cip");
                if (cipNodes.Count > 0)
                {
                    var cipParser = new AttributeParser(cipNodes[0] as XmlElement);
                    hfov = cipParser.ParseDouble("hfov", 45);
                    asp = cipParser.ParseDouble("aspect_ratio", 1);
                    cu = cipParser.ParseDouble("cu", 0);
                    cv = cipParser.ParseDouble("cv", 0);
                    k1 = cipParser.ParseDouble("k1", 0);
                    k2 = cipParser.ParseDouble("k2", 0);
                    p1 = cipParser.ParseDouble("p1", 0);
                    p2 = cipParser.ParseDouble("p2", 0);
                }

                var specialType = oldStyleSpecialType[i];
                if (specialType == SpecialCameraType.Normal || specialType == SpecialCameraType.FrontCamera)
                {
                    var param = new NormalVideoParameters();
                    param.Invert = param.Invert = invert ? 1 : 0;
                    param.VFlip = param.VFlip = vflip ? 1 : 0;
                    param.Undistort = param.Undistort = undistort ? 1 : 0;
                    param.HFOV = param.HFOV = hfov;
                    param.AspectRatio = asp;
                    param.CU = cu;
                    param.CV = cv;
                    param.K1 = k1;
                    param.K2 = k2;
                    param.K3 = k3;
                    param.K4 = k4;
                    param.K5 = k5;
                    param.K6 = k6;
                    param.P1 = p1;
                    param.P2 = p2;

                    param.SpecialHint = param.SpecialHint = specialType == SpecialCameraType.FrontCamera ? 2 : 0;

                    if (specialType == SpecialCameraType.FrontCamera)
                    {
                        var cpSpecial = new AttributeParser(frontCameraNodes[0] as XmlElement);
                        param.OX = param.OX = cpSpecial.ParseDouble("offset", -1.5);
                        param.OY = param.OY = cpSpecial.ParseDouble("offset_y", 0);
                        param.OZ = param.OZ = cpSpecial.ParseDouble("height", 1.0);
                        var autocalib = cpSpecial.ParseBool("enable_auto_calib", "yes", true);
                        if (!autocalib)
                        {
                            var centerX = cpSpecial.ParseDouble("fixed_center_x", 0);
                            var centerY = cpSpecial.ParseDouble("fixed_center_y", 0);
                            param.Yaw = -Math.Atan(centerX * 2 * Math.Tan(0.5 * param.HFOV * Math.PI / 180)) * 180 / Math.PI;
                            param.Pitch = Math.Atan(centerY * 2 * Math.Tan(0.5 * param.HFOV * Math.PI / 180)) * 180 / Math.PI;
                        }
                    }

                    channels[i].Parameters = param;
                }
                else if (specialType == SpecialCameraType.LeftLaneCamera || specialType == SpecialCameraType.RightLaneCamera)
                {
                    var param = new LaneLineVideoParameters();
                    param.Invert = invert ? 1 : 0;
                    param.VFlip = vflip ? 1 : 0;
                    param.Undistort = undistort ? 1 : 0;
                    param.HFOV = hfov;
                    param.AspectRatio = asp;
                    param.CU = cu;
                    param.CV = cv;
                    param.K1 = k1;
                    param.K2 = k2;
                    param.K3 = k3;
                    param.K4 = k4;
                    param.K5 = k5;
                    param.K6 = k6;
                    param.P1 = p1;
                    param.P2 = p2;

                    var cpSpecial = new AttributeParser((specialType == SpecialCameraType.LeftLaneCamera ? leftLaneCameraNodes[0] : rightLaneCameraNodes[0]) as XmlElement);
                    var zero = cpSpecial.ParsePoint("zero", new FloatPoint(0.25f, 0.0f)).Value;
                    var meter = cpSpecial.ParsePoint("meter", new FloatPoint(-0.25f, 0.0f)).Value;
                    var fifty = cpSpecial.ParseDouble("ratio", 0.5);
                    param.ZeroPointU = zero.X;
                    param.ZeroPointV = zero.Y;
                    param.MeterPointU = meter.X;
                    param.MeterPointV = meter.Y;
                    param.FiftyRatio = fifty;
                    param.MeterScale = 1;
                    param.SpecialHint = specialType == SpecialCameraType.LeftLaneCamera ? 0 : 1;

                    channels[i].Parameters = param;
                }
                else if (specialType == SpecialCameraType.LeftBlindSpotCamera || specialType == SpecialCameraType.RightBlindSpotCamera)
                {
                    var param = new BlindSpotParameters();
                    param.Invert = invert ? 1 : 0;
                    param.VFlip = vflip ? 1 : 0;
                    param.Undistort = undistort ? 1 : 0;
                    param.HFOV = hfov;
                    param.AspectRatio = asp;
                    param.CU = cu;
                    param.CV = cv;
                    param.K1 = k1;
                    param.K2 = k2;
                    param.K3 = k3;
                    param.K4 = k4;
                    param.K5 = k5;
                    param.K6 = k6;
                    param.P1 = p1;
                    param.P2 = p2;

                    var cpSpecial = new AttributeParser((specialType == SpecialCameraType.LeftBlindSpotCamera ? leftBSCameraNodes[0] : rightBSCameraNodes[0]) as XmlElement);
                    var p00 = cpSpecial.ParsePoint("p00", new FloatPoint(-0.25f, 0.25f)).Value;
                    var p04 = cpSpecial.ParsePoint("p04", new FloatPoint(-0.25f, -0.25f)).Value;
                    var p30 = cpSpecial.ParsePoint("p30", new FloatPoint(0.25f, 0.25f)).Value;
                    var p34 = cpSpecial.ParsePoint("p34", new FloatPoint(0.25f, -0.25f)).Value;
                    var p0hRatio = cpSpecial.ParseDouble("p0h_ratio", 0.5);
                    var p3hRatio = cpSpecial.ParseDouble("p3h_ratio", 0.5);
                    var ph0Ratio = cpSpecial.ParseDouble("ph0_ratio", 0.5);
                    var ph4Ratio = cpSpecial.ParseDouble("ph4_ratio", 0.5);
                    param.P00U = p00.X;
                    param.P00V = p00.Y;
                    param.P04U = p04.X;
                    param.P04V = p04.Y;
                    param.P30U = p30.X;
                    param.P30V = p30.Y;
                    param.P34U = p34.X;
                    param.P34V = p34.Y;
                    param.P0hRatio = p0hRatio;
                    param.P3hRatio = p3hRatio;
                    param.Ph0Ratio = ph0Ratio;
                    param.Ph4Ratio = ph4Ratio;
                    param.SpecialHint = specialType == SpecialCameraType.LeftBlindSpotCamera ? 0 : 1;

                    channels[i].Parameters = param;
                }
            }

            Devices = devices;
            Channels = channels;
        }

        public override String GetConfig()
        {
            var xml = Xml.Create("root");
            var rootNode = xml.DocumentElement;

            var rootCW = new AttributeWriter(rootNode);
            rootCW.WriteLong("hires_buffer_max_size", HiresBufferMaxSize);
            rootCW.WriteInt("max_process_thread_count", MaxProcessThreadCount);

            foreach (var dev in Devices)
            {
                var cw = new AttributeWriter(rootNode.AppendChild(xml.CreateElement("device")) as XmlElement);
                cw.WriteString("cam_type", dev.ID.Type);
                cw.WriteString("local_id", dev.ID.LocalID);
                cw.WriteInt("mode", (int)dev.Mode);
                cw.WriteBool("enabled", dev.Mode == VideoDeviceMode.Input, "yes", "no");
                cw.WriteInt("channel", dev.ChannelIndex);
                cw.WriteInt("fps", dev.FrameRate);
                cw.WriteBool("aligned_fps", dev.AlignedFPS, "yes", "no");
                if (dev.InputMode.InputCodec != VideoDataCodec.Invalid)
                {
                    cw.WriteInt("input_codec", (int)dev.InputMode.InputCodec);
                    cw.WriteInt("input_width", dev.InputMode.Size.Width);
                    cw.WriteInt("input_height", dev.InputMode.Size.Height);
                }
                if (dev.OutputMode.OutputCodec != VideoDataCodec.Invalid)
                {
                    cw.WriteInt("output_codec", (int)dev.OutputMode.OutputCodec);
                    cw.WriteInt("output_width", dev.OutputMode.Size.Width);
                    cw.WriteInt("output_height", dev.OutputMode.Size.Height);
                }
            }

            for (int i = 0; i < ChannelNumber.Video; i++)
            {
                var chNode = rootNode.AppendChild(xml.CreateElement("ch" + i)) as XmlElement;
                var cw = new AttributeWriter(chNode);
                cw.WriteString("name", Channels[i].Name);
                if (Channels[i].Parameters is NormalVideoParameters) cw.WriteString("param_type", "normal");
                else if (Channels[i].Parameters is LaneLineVideoParameters) cw.WriteString("param_type", "line");
                else if (Channels[i].Parameters is BlindSpotParameters) cw.WriteString("param_type", "bs");
                else if (Channels[i].Parameters is FisheyeVideoParameters) cw.WriteString("param_type", "fisheye");
                else if (Channels[i].Parameters is CustomizedParameters) cw.WriteString("param_type", "custom");
                Channels[i].Parameters.ToXml(chNode.AppendChild(xml.CreateElement("param")) as XmlElement);
                cw.WriteInt("remote_rec_fps", Channels[i].RemoteRecordFPS);
                cw.WriteBool("remote_aligned_fps", Channels[i].RemoteAlignedFPS, "yes", "no");
                cw.WriteInt("remote_codec", (int)Channels[i].RemoteCodec);
                cw.WriteInt("remote_width", Channels[i].RemoteSize.Width);
                cw.WriteInt("remote_height", Channels[i].RemoteSize.Height);
            }

            return xml.InnerXml;
        }

        public override async Task<(ConfigStatus, String)> GetConfigStatus()
        {
            var appMode = await AgencyAsync.GetAppMode();
            if (appMode == ApplicationMode.Replay || appMode == ApplicationMode.Offline || appMode == ApplicationMode.Remote)
            {
                return (ConfigStatus.Enabled, null);
            }
            else if (appMode == ApplicationMode.Online)
            {
                var devices = await AgencyAsync.GetVideoDevices();
                if (devices == null) return (ConfigStatus.Disabled, null);

                String errorHint = null;
                bool hasError = false, hasOK = false;
                foreach (var dev in Devices)
                {
                    if (dev.ID.Type.Length == 0) continue;
                    if (dev.Mode == VideoDeviceMode.Disabled) continue;
                    if (!IsDeviceChannelOK(dev.ID))
                    {
                        errorHint = "Device channel already used: " + dev.ID.ToString();
                        hasError = true;
                    }
                    if (devices.ContainsKey(dev.ID)) hasOK = true;
                    else
                    {
                        errorHint = "Device unavailable: " + dev.ID.ToString();
                        hasError = true;
                    }
                }

                if (hasOK)
                {
                    if (hasError) return (ConfigStatus.EnabledWithWarning, errorHint);
                    else return (ConfigStatus.Enabled, errorHint);
                }
                else
                {
                    if (hasError) return (ConfigStatus.EnabledWithError, errorHint);
                    else return (ConfigStatus.Disabled, errorHint);
                }
            }
            else return (ConfigStatus.Disabled, null);
        }

        public override void DisableAll()
        {
            foreach (var dev in Devices)
            {
                dev.Mode = VideoDeviceMode.Disabled;
            }
            foreach (var ch in Channels)
            {
                ch.RemoteCodec = VideoDataCodec.Invalid;
            }
        }

        public override async Task DisableErrorPart()
        {
            var devices = await AgencyAsync.GetVideoDevices();
            foreach (var dev in Devices)
            {
                if (!IsDeviceChannelOK(dev.ID)) dev.Mode = VideoDeviceMode.Disabled;
                if (devices != null && !devices.ContainsKey(dev.ID)) dev.Mode = VideoDeviceMode.Disabled;
            }
        }

        public bool IsDeviceChannelOK(VideoDeviceID config)
        {
            var flags = new bool[ChannelNumber.Video];
            foreach (var c in Devices)
            {
                if (c.ChannelIndex < 0 || c.ChannelIndex >= ChannelNumber.Video) continue;
                if (c.Mode == VideoDeviceMode.Disabled) continue;
                if (c.ID.Equals(config))
                {
                    return !flags[c.ChannelIndex];
                }
                else
                {
                    flags[c.ChannelIndex] = true;
                }
            }
            return false;
        }

        public void SetChannelDevices(VideoChannelDevice[] devices)
        {
            if (devices.Length != ChannelNumber.Video) return;

            var deviceConfigs = new List<VideoDeviceConfig>();
            var channels = new VideoChannelConfig[ChannelNumber.Video];
            for (int i = 0; i < ChannelNumber.Video; i++)
            {
                channels[i] = new VideoChannelConfig();

                if (devices[i] == null) continue;
                if (String.IsNullOrEmpty(devices[i].Name)) continue;
                if (devices[i].ID == null) continue;
                if (devices[i].Codec == VideoDataCodec.Invalid) continue;
                if (devices[i].Size.Width <= 0 || devices[i].Size.Height <= 0) continue;
                if (devices[i].HFOV <= 0) continue;

                var devConfig = new VideoDeviceConfig();
                devConfig.ID = devices[i].ID;
                devConfig.Mode = devices[i].IsOutput ? VideoDeviceMode.Output : VideoDeviceMode.Input;
                devConfig.ChannelIndex = i;
                devConfig.FrameRate = devices[i].FrameRate;
                devConfig.AlignedFPS = false;
                if (devices[i].IsOutput) devConfig.OutputMode = new VideoOutputMode{ OutputCodec = devices[i].Codec, Size = devices[i].Size };
                else devConfig.InputMode = new VideoInputMode{ InputCodec = devices[i].Codec, Size = devices[i].Size };
                deviceConfigs.Add(devConfig);

                channels[i].Name = devices[i].Name;
                
                var param = channels[i].Parameters as NormalVideoParameters;
                param.Invert = devices[i].Invert ? 1 : 0;
                param.VFlip = devices[i].VFlip ? 1 : 0;
                param.HFOV = devices[i].HFOV;
            }

            Devices = deviceConfigs;
            Channels = channels;
        }

        // Config
        public List<VideoDeviceConfig> Devices { get; set; }
        public VideoChannelConfig[] Channels { get; set; }
        public long HiresBufferMaxSize { get; set; }
        public int MaxProcessThreadCount { get; set; }
    }
}
